今天要介紹的 Pattern 是 Template Pattern。個人覺得在 Design Patterns 中,Template Pattern 大概是數一數二常用卻不自知的 Pattern 了。
在軟體開發中,很常出現一個情形,今天開發了一個功能 A,下次被要求開發一個很類似的功能 B。例如,你今天做了一個報表輸出成 JSON 格式的功能,可是明天被要求支援輸出成 HTML 格式。這時候有多種選擇,可以簡單地複製功能 A 的程式碼,然後修改成功能 B,或是使用今天要介紹的 Template Pattern。

Template pattern的概念是把一個功能拆成多個小步驟,每個小步驟就是一個method,然後用一個template method把這些小步驟組合起來變成一個功能。其中的小步驟可以是沒有具體的內容,也可以是已經有預設行為的。然後把這些小步驟跟template method放到一個base class,然後再去創造sub class來繼承base class並改寫需要的小步驟。
因此 Template Pattern 特別適合用在多個相似的類別(class),彼此之間有共同的架構與邏輯,但卻有微小的差異。
使用 Ruby,以報表輸出為例,我們原本的程式碼可能像這樣。
require 'json'
class JsonReport
  def initialize(title, body, footnote)
    @title = title
    @body = body
    @footnote = footnote
  end
  def print_report
    puts "This report is printed on #{Time.now.strftime('%Y-%m-%d')}"
    puts JSON.dump({title: @title,
                    body: @body,
                    footnote: @footnote})
  end
end
現在我們被要求加入支援 HTML 格式的功能,如果要使用 Template Pattern 的方式,我們會把 JsonReport 跟 HtmlReport 共通的部分獨立出來變成 BaseReport,然後 JsonReport 和 HtmlReport 繼承 BaseReport ,再把彼此差異的地方,放在 JsonReport 或 HtmlReport 中。

require 'json'
class BaseReport
  def initialize(title, body, footnote)
    @title = title
    @body = body
    @footnote = footnote
  end
  def print_report
    print_timestamp()
    print_author()
    print_contet()
  end
  private
  def print_timestamp
    puts "This report is printed on #{Time.now.strftime('%Y-%m-%d')}."
  end
  def print_author
    puts "This report is made by PicCollage."
  end
  def print_contet
    puts content
  end
  def content
    raise NotImplementedError
  end
end
class JsonReport < BaseReport
  private
  def content
    JSON.dump({title: @title,
               body: @body,
               footnote: @footnote})
  end
end
class HtmlReport < BaseReport
  private
  def content
    <<~HTML_CONTENT
      <!DOCTYPE html>
      <html>
        <head>
          <title>#{@title}</title>
        </head>
        <body>
          <p>#{@body}</p>
          <footer>
            <p>#{@footnote}</p>
          </footer>
        </body>
      </html>
    HTML_CONTENT
  end
end
pronto 是一個自動化 code review 的 Ruby 套件,為了要支援在不同的平台(GitHub, Gitlab, Bitbucket)都可以留下 code review 評論, pronto 就使用 Template Pattern 的概念來處理。
以下是 pronto 的部份的程式碼,format() 就是一個 Template Method,裡面的 client_module() 和 pretty_name() 則是由相對應的 GithubFormatter, GitlabFormatter,與 BitbucketFormatter 來處理。
module Pronto
  module Formatter
    class GitFormatter < Base
      def format(messages, repo, patches)
        client = client_module.new(repo)
        existing = existing_comments(messages, client, repo)
        comments = new_comments(messages, patches)
        additions = remove_duplicate_comments(existing, comments)
        submit_comments(client, additions)
        approve_pull_request(comments.count, additions.count, client) if defined?(self.approve_pull_request)
        "#{additions.count} Pronto messages posted to #{pretty_name}"
      end
      def client_module
        raise NotImplementedError
      end
      def pretty_name
        raise NotImplementedError
      end
      #....
    end
  end
end
module Pronto
  module Formatter
    class GithubFormatter < CommitFormatter
      def client_module
        Github
      end
      def pretty_name
        'GitHub'
      end
      def line_number(message, _)
        message.line.commit_line.position if message.line
      end
    end
  end
end
module Pronto
  module Formatter
    class GitlabFormatter < CommitFormatter
      def client_module
        Gitlab
      end
      def pretty_name
        'GitLab'
      end
      def line_number(message, _)
        message.line.commit_line.new_lineno if message.line
      end
    end
  end
end
module Pronto
  module Formatter
    class BitbucketFormatter < CommitFormatter
      def client_module
        Bitbucket
      end
      def pretty_name
        'BitBucket'
      end
      def line_number(message, _)
        message.line.new_lineno if message.line
      end
    end
  end
end
Template Pattern 可以讓你的程式碼比較 DRY (Don't Repeat Yourself),另外因為一個大功能已經被拆成許多小步驟,修改小步驟通常也比直接修改一個大功能來的安全。
但是使用 Template Pattern 也會讓你受制於 base class 所定下的設計,因而失去了一些彈性。另外使用 Template Pattern 的時候,要小心不要為了重用部分的功能,而把不太相關的 class 硬湊在一起,不正確的抽象化常常會讓程式碼變得很難維護。
作者:Maso
(下篇預告:Strategy 策略模式)